@heycys/gallery 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (292) hide show
  1. package/THIRD-PARTY-NOTICES.md +2 -2
  2. package/browser/editor/components/align.mjs +58 -0
  3. package/browser/editor/components/code/index.mjs +1 -1
  4. package/browser/editor/components/embed/page.mjs +1 -1
  5. package/browser/editor/components/file/page.mjs +4 -4
  6. package/browser/editor/components/heading/index.mjs +7 -6
  7. package/browser/editor/components/icons.mjs +3 -0
  8. package/browser/editor/components/image/index.mjs +2 -0
  9. package/browser/editor/components/image/page.mjs +172 -90
  10. package/browser/editor/components/installed/index.mjs +6 -2
  11. package/browser/editor/components/installed/page.mjs +80 -13
  12. package/browser/editor/components/media/icons.mjs +13 -8
  13. package/browser/editor/components/media/page.mjs +5 -3
  14. package/browser/editor/components/media/toolbar.mjs +12 -3
  15. package/browser/editor/components/paragraph/index.mjs +4 -3
  16. package/browser/editor/components/registry.mjs +17 -5
  17. package/browser/editor/components/table/index.mjs +4 -2
  18. package/browser/editor/page/after/index.mjs +288 -0
  19. package/browser/editor/page/align.mjs +91 -0
  20. package/browser/editor/page/beside.mjs +5 -6
  21. package/browser/editor/page/block-menu/index.mjs +6 -3
  22. package/browser/editor/page/blocks.mjs +3 -1
  23. package/browser/editor/page/board/hint.mjs +64 -27
  24. package/browser/editor/page/board/index.mjs +160 -71
  25. package/browser/editor/page/board/motion.mjs +177 -0
  26. package/browser/editor/page/board/settings.mjs +4 -3
  27. package/browser/editor/page/clipboard/index.mjs +10 -3
  28. package/browser/editor/page/clipboard/own/blocks.mjs +3 -4
  29. package/browser/editor/page/clipboard/own/describe.mjs +6 -2
  30. package/browser/editor/page/commit.mjs +2 -2
  31. package/browser/editor/page/core.mjs +3 -1
  32. package/browser/editor/page/custom-page.mjs +15 -11
  33. package/browser/editor/page/editor.mjs +31 -14
  34. package/browser/editor/page/history.mjs +19 -4
  35. package/browser/editor/page/insert/index.mjs +20 -22
  36. package/browser/editor/page/menu.mjs +15 -7
  37. package/browser/editor/page/page-objects.mjs +3 -7
  38. package/browser/editor/page/present-links.mjs +5 -4
  39. package/browser/editor/page/present-shell.mjs +63 -0
  40. package/browser/editor/page/selection/index.mjs +10 -2
  41. package/browser/editor/page/table/binding.mjs +87 -0
  42. package/browser/editor/page/table/icons.mjs +0 -2
  43. package/browser/editor/page/table/index.mjs +10 -14
  44. package/browser/editor/page/table/views.mjs +5 -52
  45. package/browser/editor/page/table/widths.mjs +82 -10
  46. package/browser/editor/page/typing/index.mjs +44 -12
  47. package/browser/editor/page/typing/leave.mjs +19 -5
  48. package/browser/shared/block-packs.mjs +23 -8
  49. package/browser/shared/component-catalog.mjs +15 -8
  50. package/browser/shared/component-source.mjs +3 -1
  51. package/browser/shared/components/heading.mjs +2 -1
  52. package/browser/shared/components/installed.mjs +1 -1
  53. package/browser/shared/components/page.mjs +2 -0
  54. package/browser/shared/components/paragraph.mjs +8 -1
  55. package/browser/shared/components/table.mjs +1 -0
  56. package/browser/shared/emoji/line-icons.mjs +37 -0
  57. package/browser/shared/http.mjs +9 -0
  58. package/browser/shared/i18n/en.mjs +40 -17
  59. package/browser/shared/outline.mjs +30 -2
  60. package/browser/shared/pack-manifest.mjs +12 -1
  61. package/browser/shared/pages.mjs +169 -57
  62. package/browser/shared/pinyin.mjs +92 -0
  63. package/browser/shared/present-step.mjs +17 -0
  64. package/browser/shared/sidebar-head.mjs +70 -0
  65. package/browser/shared/web-links.mjs +5 -0
  66. package/dist/chunk-2OHANJ6Q.mjs +2 -0
  67. package/dist/{chunk-QOUEFUCH.mjs → chunk-3MBRL2PM.mjs} +1 -1
  68. package/dist/{chunk-NWR2LT5B.mjs → chunk-4OKWZY5E.mjs} +1 -1
  69. package/dist/{chunk-RMECZA2K.mjs → chunk-5K44HLTZ.mjs} +1 -1
  70. package/dist/{chunk-NFMCP2HI.mjs → chunk-5XA2ZBAE.mjs} +2 -2
  71. package/dist/{chunk-OUUP3WKT.mjs → chunk-67N2QJYT.mjs} +1 -1
  72. package/dist/chunk-6KBYRA2C.mjs +2 -0
  73. package/dist/{chunk-SX4LJYT6.mjs → chunk-6N3PX4XX.mjs} +1 -1
  74. package/dist/{chunk-RFDZXFJF.mjs → chunk-7JDX4E65.mjs} +1 -1
  75. package/dist/chunk-7M2BQQGZ.mjs +2 -0
  76. package/dist/{chunk-ZUNVAXLH.mjs → chunk-7OG26PGF.mjs} +1 -1
  77. package/dist/{chunk-B4P2CM2J.mjs → chunk-AHN3NFKJ.mjs} +1 -1
  78. package/dist/chunk-ALZ6ZENZ.mjs +3 -0
  79. package/dist/{chunk-UGWYKHBL.mjs → chunk-AR5D57ZD.mjs} +2 -2
  80. package/dist/{chunk-W3BGBJQW.mjs → chunk-BFJGHSQN.mjs} +1 -1
  81. package/dist/{chunk-HLYV7RIH.mjs → chunk-CMA72MAG.mjs} +1 -1
  82. package/dist/{chunk-D4CKRD3V.mjs → chunk-DAZL73VK.mjs} +2 -2
  83. package/dist/{chunk-6VTU6KEO.mjs → chunk-DJISHPUL.mjs} +1 -1
  84. package/dist/{chunk-BPL64HGA.mjs → chunk-EAIFITAV.mjs} +1 -1
  85. package/dist/chunk-EP25HDAJ.mjs +2 -0
  86. package/dist/{chunk-6EZ7FNIK.mjs → chunk-F4ZA6JRZ.mjs} +1 -1
  87. package/dist/{chunk-3KJHLHUO.mjs → chunk-F6HVDZR4.mjs} +1 -1
  88. package/dist/{chunk-3NJACR4W.mjs → chunk-FDMAXMQA.mjs} +1 -1
  89. package/dist/chunk-FETSREUN.mjs +5 -0
  90. package/dist/{chunk-RKPWLGZ3.mjs → chunk-GHOYKMQ3.mjs} +1 -1
  91. package/dist/chunk-HDWTZ763.mjs +12 -0
  92. package/dist/{chunk-VTKNEYT3.mjs → chunk-HPOI4ZX7.mjs} +1 -1
  93. package/dist/{chunk-OW5NWPOJ.mjs → chunk-HXG7EOPJ.mjs} +1 -1
  94. package/dist/{chunk-V6RIYQZZ.mjs → chunk-ITOH5EGO.mjs} +1 -1
  95. package/dist/{chunk-ZWPMCK7P.mjs → chunk-IV64FFI4.mjs} +1 -1
  96. package/dist/chunk-J43Z6LSS.mjs +2 -0
  97. package/dist/chunk-J5PUIV3Y.mjs +4 -0
  98. package/dist/{chunk-KTKHRC6H.mjs → chunk-JA7G53PC.mjs} +1 -1
  99. package/dist/{chunk-NWFBE2YA.mjs → chunk-JLTZF4LA.mjs} +1 -1
  100. package/dist/{chunk-GY3FAUVM.mjs → chunk-JZT7I2OJ.mjs} +1 -1
  101. package/dist/{chunk-QKXMPUXC.mjs → chunk-K5XATEOO.mjs} +1 -1
  102. package/dist/chunk-L2SUIS5S.mjs +2 -0
  103. package/dist/{chunk-TEZAWDSQ.mjs → chunk-LM4XFQHZ.mjs} +1 -1
  104. package/dist/{chunk-5ZJA7CXT.mjs → chunk-MN7BZL4R.mjs} +1 -1
  105. package/dist/chunk-MTYYHM2G.mjs +15 -0
  106. package/dist/{chunk-HMYTM3GT.mjs → chunk-MXUBKDRO.mjs} +1 -1
  107. package/dist/{chunk-MHW7ZKOU.mjs → chunk-OEXWRJME.mjs} +1 -1
  108. package/dist/{chunk-CRJEG7MT.mjs → chunk-OFHXGWOX.mjs} +1 -1
  109. package/dist/chunk-ORID3AE7.mjs +166 -0
  110. package/dist/{chunk-32ZUEFZ7.mjs → chunk-PGDE24N5.mjs} +1 -1
  111. package/dist/{chunk-UNOYVIYR.mjs → chunk-POTA2FV4.mjs} +1 -1
  112. package/dist/{chunk-N7AZZLGT.mjs → chunk-QE5RG76S.mjs} +18 -17
  113. package/dist/{chunk-N76HWBA3.mjs → chunk-R2ZDLJYT.mjs} +1 -1
  114. package/dist/chunk-SWUTTZCN.mjs +2 -0
  115. package/dist/{chunk-POQSE4C2.mjs → chunk-TR7TWGHB.mjs} +1 -1
  116. package/dist/chunk-U5XL7D6A.mjs +3 -0
  117. package/dist/{chunk-PVER7LSC.mjs → chunk-U7NZI6N4.mjs} +2 -2
  118. package/dist/{chunk-Q47PLKIJ.mjs → chunk-UGWMMEJ5.mjs} +1 -1
  119. package/dist/chunk-UJQJKNGK.mjs +2 -0
  120. package/dist/chunk-UYVCQK3H.mjs +22 -0
  121. package/dist/{chunk-FJYN7RTH.mjs → chunk-VFX2KNHP.mjs} +1 -1
  122. package/dist/{chunk-UBBRTJ2R.mjs → chunk-VS7JIGJW.mjs} +1 -1
  123. package/dist/{chunk-NVRNLK25.mjs → chunk-W7665ZEC.mjs} +2 -2
  124. package/dist/{chunk-NG375ILQ.mjs → chunk-WQDQWCX5.mjs} +1 -1
  125. package/dist/chunk-YLPSMDIS.mjs +5 -0
  126. package/dist/{chunk-VXACTLO3.mjs → chunk-YNAWBAYN.mjs} +1 -1
  127. package/dist/{chunk-ZB4H7ZN4.mjs → chunk-YROX2O22.mjs} +1 -1
  128. package/dist/{chunk-YQGT4AB2.mjs → chunk-ZDLPUOAG.mjs} +1 -1
  129. package/dist/chunk-ZQS6H473.mjs +16 -0
  130. package/dist/{chunk-OJKMINIU.mjs → chunk-ZWWC4KLF.mjs} +15 -15
  131. package/dist/cli.mjs +1 -1
  132. package/dist/server.mjs +1 -1
  133. package/dist/worker.mjs +1 -1
  134. package/kit/board.css +5 -5
  135. package/kit/code.js +15 -15
  136. package/kit/report.css +120 -41
  137. package/kit/runtime.js +45 -7
  138. package/kit/tables.js +2 -0
  139. package/kit/ui.js +2 -0
  140. package/package.json +1 -1
  141. package/packs/gallery/chart-view/kit.js +3 -3
  142. package/packs/gallery/chart-view/writing.md +1 -1
  143. package/packs/gallery/countdown/component.json +1 -1
  144. package/packs/gallery/countdown/kit.js +89 -29
  145. package/packs/gallery/countdown/preview.svg +1 -1
  146. package/packs/gallery/countdown/style.css +88 -100
  147. package/packs/gallery/countdown/writing.md +2 -2
  148. package/packs/gallery/habit/component.json +3 -3
  149. package/packs/gallery/habit/kit.js +114 -75
  150. package/packs/gallery/habit/preview.svg +1 -1
  151. package/packs/gallery/habit/style.css +133 -205
  152. package/packs/gallery/habit/writing.md +2 -2
  153. package/packs/gallery/heatmap/component.json +2 -2
  154. package/packs/gallery/heatmap/kit.js +335 -173
  155. package/packs/gallery/heatmap/preview.svg +1 -1
  156. package/packs/gallery/heatmap/style.css +158 -170
  157. package/packs/gallery/heatmap/writing.md +2 -2
  158. package/packs/gallery/kanban/kit.js +3 -3
  159. package/packs/gallery/kanban/writing.md +1 -1
  160. package/packs/gallery/metric/kit.js +3 -3
  161. package/packs/gallery/metric/writing.md +1 -1
  162. package/packs/gallery/note/component.json +3 -3
  163. package/packs/gallery/note/kit.js +40 -13
  164. package/packs/gallery/note/preview.svg +1 -1
  165. package/packs/gallery/note/style.css +51 -30
  166. package/packs/gallery/note/writing.md +2 -2
  167. package/packs/gallery/recent/component.json +2 -2
  168. package/packs/gallery/recent/kit.js +141 -86
  169. package/packs/gallery/recent/preview.svg +1 -1
  170. package/packs/gallery/recent/style.css +53 -126
  171. package/packs/gallery/recent/writing.md +2 -2
  172. package/packs/gallery/static/component.json +2 -2
  173. package/packs/gallery/static/preview.svg +1 -1
  174. package/packs/gallery/static/style.css +99 -66
  175. package/packs/gallery/static/writing.md +2 -2
  176. package/packs/gallery/stats/component.json +3 -3
  177. package/packs/gallery/stats/kit.js +253 -72
  178. package/packs/gallery/stats/preview.svg +1 -1
  179. package/packs/gallery/stats/style.css +64 -142
  180. package/packs/gallery/stats/writing.md +2 -2
  181. package/packs/gallery/today/component.json +2 -2
  182. package/packs/gallery/today/kit.js +62 -27
  183. package/packs/gallery/today/preview.svg +1 -1
  184. package/packs/gallery/today/style.css +101 -72
  185. package/packs/gallery/today/writing.md +2 -2
  186. package/packs/gallery/todos/component.json +1 -1
  187. package/packs/gallery/todos/kit.js +218 -71
  188. package/packs/gallery/todos/preview.svg +1 -1
  189. package/packs/gallery/todos/style.css +170 -223
  190. package/packs/gallery/todos/writing.md +1 -1
  191. package/packs/gallery/weather/component.json +2 -2
  192. package/packs/gallery/weather/kit.js +165 -90
  193. package/packs/gallery/weather/preview.svg +1 -1
  194. package/packs/gallery/weather/style.css +100 -218
  195. package/packs/gallery/weather/writing.md +1 -1
  196. package/packs/gallery-soft/countdown/component.json +18 -0
  197. package/packs/gallery-soft/countdown/example.html +3 -0
  198. package/packs/gallery-soft/countdown/kit.js +206 -0
  199. package/packs/gallery-soft/countdown/preview.svg +1 -0
  200. package/packs/gallery-soft/countdown/style.css +211 -0
  201. package/packs/gallery-soft/countdown/template.html +3 -0
  202. package/packs/gallery-soft/countdown/writing.md +24 -0
  203. package/packs/gallery-soft/habit/component.json +14 -0
  204. package/packs/gallery-soft/habit/example.html +3 -0
  205. package/packs/gallery-soft/habit/kit.js +366 -0
  206. package/packs/gallery-soft/habit/preview.svg +1 -0
  207. package/packs/gallery-soft/habit/style.css +251 -0
  208. package/packs/gallery-soft/habit/template.html +3 -0
  209. package/packs/gallery-soft/habit/writing.md +23 -0
  210. package/packs/gallery-soft/heatmap/component.json +18 -0
  211. package/packs/gallery-soft/heatmap/example.html +3 -0
  212. package/packs/gallery-soft/heatmap/kit.js +498 -0
  213. package/packs/gallery-soft/heatmap/preview.svg +1 -0
  214. package/packs/gallery-soft/heatmap/style.css +181 -0
  215. package/packs/gallery-soft/heatmap/template.html +3 -0
  216. package/packs/gallery-soft/heatmap/writing.md +23 -0
  217. package/packs/gallery-soft/note/component.json +14 -0
  218. package/packs/gallery-soft/note/example.html +3 -0
  219. package/packs/gallery-soft/note/kit.js +162 -0
  220. package/packs/gallery-soft/note/preview.svg +1 -0
  221. package/packs/gallery-soft/note/style.css +148 -0
  222. package/packs/gallery-soft/note/template.html +3 -0
  223. package/packs/gallery-soft/note/writing.md +20 -0
  224. package/packs/gallery-soft/recent/component.json +13 -0
  225. package/packs/gallery-soft/recent/example.html +1 -0
  226. package/packs/gallery-soft/recent/kit.js +335 -0
  227. package/packs/gallery-soft/recent/preview.svg +1 -0
  228. package/packs/gallery-soft/recent/style.css +160 -0
  229. package/packs/gallery-soft/recent/template.html +1 -0
  230. package/packs/gallery-soft/recent/writing.md +17 -0
  231. package/packs/gallery-soft/static/component.json +13 -0
  232. package/packs/gallery-soft/static/example.html +12 -0
  233. package/packs/gallery-soft/static/preview.svg +1 -0
  234. package/packs/gallery-soft/static/style.css +186 -0
  235. package/packs/gallery-soft/static/template.html +1 -0
  236. package/packs/gallery-soft/static/writing.md +20 -0
  237. package/packs/gallery-soft/stats/component.json +13 -0
  238. package/packs/gallery-soft/stats/example.html +1 -0
  239. package/packs/gallery-soft/stats/kit.js +341 -0
  240. package/packs/gallery-soft/stats/preview.svg +1 -0
  241. package/packs/gallery-soft/stats/style.css +119 -0
  242. package/packs/gallery-soft/stats/template.html +1 -0
  243. package/packs/gallery-soft/stats/writing.md +13 -0
  244. package/packs/gallery-soft/today/component.json +12 -0
  245. package/packs/gallery-soft/today/example.html +1 -0
  246. package/packs/gallery-soft/today/kit.js +222 -0
  247. package/packs/gallery-soft/today/preview.svg +1 -0
  248. package/packs/gallery-soft/today/style.css +178 -0
  249. package/packs/gallery-soft/today/template.html +1 -0
  250. package/packs/gallery-soft/today/writing.md +16 -0
  251. package/packs/gallery-soft/todos/component.json +19 -0
  252. package/packs/gallery-soft/todos/example.html +3 -0
  253. package/packs/gallery-soft/todos/kit.js +527 -0
  254. package/packs/gallery-soft/todos/preview.svg +1 -0
  255. package/packs/gallery-soft/todos/style.css +212 -0
  256. package/packs/gallery-soft/todos/template.html +3 -0
  257. package/packs/gallery-soft/todos/writing.md +32 -0
  258. package/packs/gallery-soft/weather/component.json +19 -0
  259. package/packs/gallery-soft/weather/example.html +3 -0
  260. package/packs/gallery-soft/weather/kit.js +411 -0
  261. package/packs/gallery-soft/weather/preview.svg +1 -0
  262. package/packs/gallery-soft/weather/style.css +185 -0
  263. package/packs/gallery-soft/weather/template.html +3 -0
  264. package/packs/gallery-soft/weather/writing.md +28 -0
  265. package/release.json +9 -9
  266. package/skill/SKILL.md +12 -12
  267. package/skill/references/building-blocks.md +4 -4
  268. package/skill/references/pages-and-data.md +13 -9
  269. package/skill/references/writing-html.md +2 -0
  270. package/web/assets/index-BM_K-Auu.css +1 -0
  271. package/web/assets/index-DRslOBiv.js +15 -0
  272. package/web/index.html +2 -2
  273. package/dist/chunk-2NRBY6AJ.mjs +0 -2
  274. package/dist/chunk-35NQ4PLQ.mjs +0 -2
  275. package/dist/chunk-4AV456RQ.mjs +0 -2
  276. package/dist/chunk-5XYHDBJ5.mjs +0 -2
  277. package/dist/chunk-6A53MZYZ.mjs +0 -2
  278. package/dist/chunk-6EXH3ORZ.mjs +0 -166
  279. package/dist/chunk-BVEFGE4B.mjs +0 -2
  280. package/dist/chunk-D4EURAX4.mjs +0 -4
  281. package/dist/chunk-DQCE7TJY.mjs +0 -5
  282. package/dist/chunk-FWNRPMCJ.mjs +0 -12
  283. package/dist/chunk-FWW4CSAZ.mjs +0 -2
  284. package/dist/chunk-JVHHHGEA.mjs +0 -3
  285. package/dist/chunk-PZDLJJTJ.mjs +0 -3
  286. package/dist/chunk-SXHBYZST.mjs +0 -16
  287. package/dist/chunk-UFNYFFKO.mjs +0 -20
  288. package/dist/chunk-UKSHDZZ6.mjs +0 -2
  289. package/dist/chunk-VJPFVNNW.mjs +0 -15
  290. package/dist/chunk-VS3ZMVGX.mjs +0 -5
  291. package/web/assets/index-BK2e_07w.js +0 -13
  292. package/web/assets/index-CWFI83mF.css +0 -1
@@ -1,6 +1,6 @@
1
1
  # Third-party notices
2
2
 
3
- @heycys/gallery 0.1.2 carries the following code and data of others, bundled into dist/ and web/, built into kit/, or as files in vendor/ and browser/.
3
+ @heycys/gallery 0.2.0 carries the following code and data of others, bundled into dist/ and web/, built into kit/, or as files in vendor/ and browser/.
4
4
 
5
5
  ## @asamuzakjp/css-color 5.1.11
6
6
 
@@ -3254,7 +3254,7 @@ SOFTWARE.
3254
3254
 
3255
3255
  ## Lucide icons
3256
3256
 
3257
- A few line icons are Lucide's (https://lucide.dev), as lucide-react 1.67.0 has them: the workbench's sidebar toggle, search, 新建 and settings gear, the 写作偏好 book, and the icon of a page with none (the workbench, the editor, the kit's page block, the browser tab).
3257
+ A few line icons are Lucide's (https://lucide.dev), as lucide-react 1.67.0 has them: the workbench's sidebar toggle, search, 新建 and settings gear, the 写作偏好 book, the icon of a page with none (the workbench, the editor, the kit's page block, the browser tab), and the default home page's house.
3258
3258
 
3259
3259
  ```
3260
3260
  ISC License
@@ -0,0 +1,58 @@
1
+ // A block's alignment, and a text block's indent (对齐与缩进, the block menu's: Feishu's 缩进和对齐 ›;
2
+ // components that say `toggles: ALIGN_TOGGLES`, a paragraph and a heading; a table, whose frame is placed
3
+ // on the page the same way, says `toggles: ALIGN_PLACES`). Class switches the file keeps and the
4
+ // kit draws (src/render/kit/styles/flow.css, table.css): centred or right-aligned (doc-align-center,
5
+ // doc-align-right; left is neither, the kit's own), and for a text block moved in by levels of two body
6
+ // characters (doc-indent-1 … doc-indent-8). The page editor's block menu sets them (page/align.mjs), Enter carries them
7
+ // to the block it makes (core.mjs newBlock), and Backspace at a block's start takes them off one at a
8
+ // time before it joins or deletes (typing/: the alignment first, then a level of indent, Feishu's
9
+ // order).
10
+
11
+ import { TEXT_ALIGN_CLASSES } from "../../shared/components/paragraph.mjs";
12
+
13
+ /** The alignments a text block may have, by name: none is left (the kit's own). */
14
+ export const ALIGNS = { center: "doc-align-center", right: "doc-align-right" };
15
+
16
+ /** The indents' classes, level 1 first (the kit's: src/shared/components/paragraph.mjs). */
17
+ export const INDENTS = TEXT_ALIGN_CLASSES.filter((name) => name.startsWith("doc-indent-"));
18
+
19
+ /** The deepest indent. */
20
+ export const INDENT_MOST = INDENTS.length;
21
+
22
+ /** The classes of a text block's alignment and indent: its component's `toggles`. */
23
+ export const ALIGN_TOGGLES = TEXT_ALIGN_CLASSES;
24
+
25
+ /** The classes of a block's alignment alone (a table's frame: its component's `toggles`), the same two. */
26
+ export const ALIGN_PLACES = Object.values(ALIGNS);
27
+
28
+ /** The elements that take a text block's alignment and indent (a paragraph, a heading; a cell's alignment is its own classes'). */
29
+ export const TEXT_BLOCKS = "p, h1, h2, h3, h4";
30
+
31
+ /** A block's alignment: "left", "center" or "right". */
32
+ export const alignOf = (element) => Object.keys(ALIGNS).find((name) => element.classList.contains(ALIGNS[name])) ?? "left";
33
+
34
+ /** A block's indent: 0 to INDENT_MOST. */
35
+ export const indentOf = (element) => INDENTS.findIndex((name) => element.classList.contains(name)) + 1;
36
+
37
+ /** The look (class switches) that gives a block the alignment `name` ("left": none). */
38
+ export const alignLook = (name) => ({ classes: Object.fromEntries(Object.entries(ALIGNS).map(([each, cls]) => [cls, each === name])) });
39
+
40
+ /** The look that gives a block the indent `level` (0: none). */
41
+ export const indentLook = (level) => ({ classes: Object.fromEntries(INDENTS.map((cls, index) => [cls, index + 1 === level])) });
42
+
43
+ /**
44
+ * The alignment and indent of text block `from` (a paragraph or a heading) given to `to`, a paragraph
45
+ * made from it (Enter's split: core.mjs newBlock, the half after the caret or the empty line before it,
46
+ * as a word processor's new line keeps its paragraph's). Returns `to`.
47
+ */
48
+ export function carry(from, to) {
49
+ if (!from?.matches?.(TEXT_BLOCKS)) return to;
50
+ for (const name of ALIGN_TOGGLES) if (from.classList.contains(name)) to.classList.add(name);
51
+ return to;
52
+ }
53
+
54
+ /** Whether a block takes an alignment (its kind's toggles hold it; `toggles`: TOGGLES of its kind). */
55
+ export const aligns = (toggles) => Boolean(toggles?.includes(ALIGNS.center));
56
+
57
+ /** Whether a block takes an indent too, as a text block does (a table's alignment has none). */
58
+ export const indents = (toggles) => Boolean(toggles?.includes(INDENTS[0]));
@@ -67,7 +67,7 @@ export default {
67
67
  components: ["doc-code"],
68
68
  placeholders: { title: t("请输入代码块名称") }, // Feishu's
69
69
  hosts: {
70
- code: { name: t("代码块"), caps: CODE_CAPS, opensBefore: true, textFilters: false, closed: true, inner: (element) => escapeText(codeText(element)) }, // plain text, its lines kept
70
+ code: { name: t("代码块"), caps: CODE_CAPS, opensBefore: false, textFilters: false, closed: true, inner: (element) => escapeText(codeText(element)) }, // Enter stays inside its code, including at the start
71
71
  codeTitle: { name: t("代码块标题"), caps: CODE_CAPS, parts: [["code", "title"]] }, // one line; empty, the kit shows 代码块
72
72
  },
73
73
  types: {
@@ -37,7 +37,7 @@ const STYLE = `
37
37
  ${gestureLock("data-ge-embed-resize=\"right\"", { cursor: "nwse-resize" })}
38
38
  ${gestureLock("data-ge-embed-resize=\"left\"", { cursor: "nesw-resize" })}
39
39
  .ge-layer > .ge-embed-corner { pointer-events: auto; }
40
- /* A corner handle: a plain gray bar bent round the frame's corner, no border (as the image's sides). */
40
+ /* A corner handle: a plain gray bar bent round the frame's corner, no border. */
41
41
  .ge-embed-corner { width: 20px; height: 20px; cursor: nwse-resize; }
42
42
  .ge-embed-corner[data-side="left"] { cursor: nesw-resize; }
43
43
  .ge-embed-corner::after { content: ""; position: absolute; bottom: 5px; width: 9px; height: 9px; border: 0 solid light-dark(rgb(55 53 47 / 35%), rgb(255 255 255 / 38%)); border-bottom-width: 3px; transition: border-color 120ms ease; }
@@ -58,9 +58,9 @@ export function create({ pages, fileUrl }) {
58
58
  onEmbed: (url) => fetchInto(figure, url),
59
59
  });
60
60
 
61
- async function uploadInto(figure, file) {
61
+ async function uploadInto(figure, file, options = {}) {
62
62
  if (file.size > MAX_BYTES) return say(t("文件太大(最多 {0})。", sizeLimit("file")), "error");
63
- return media.attempt(figure, t("正在上传 {0}…", file.name), t("无法上传这个文件"), async () => place(figure, await media.uploadFile(file)));
63
+ return media.attempt(figure, t("正在上传 {0}…", file.name), t("无法上传这个文件"), async () => place(figure, await media.uploadFile(file), options));
64
64
  }
65
65
 
66
66
  const fetchInto = (figure, url) => media.attempt(figure, t("正在下载…"), t("无法下载这个文件"), async () => place(figure, await media.uploadUrl(url)));
@@ -69,13 +69,13 @@ export function create({ pages, fileUrl }) {
69
69
  // with its caption), never the same element, so that undo brings back what stood there (a
70
70
  // placeholder never written, a block as it was) and the file loses the new one. One undo step; the
71
71
  // block selected.
72
- function place(figure, answer) {
72
+ function place(figure, answer, { pasted = false } = {}) {
73
73
  if (!figure.isConnected) return;
74
74
  const doc = figure.ownerDocument;
75
75
  const replacing = Boolean(linkOf(figure));
76
76
  const next = newFile(doc);
77
77
  next.replaceChildren(...fileParts(doc, answer, fileUrl(answer.path), captionOf(figure)?.innerHTML ?? ""));
78
- media.place(figure, next, { replacing });
78
+ media.place(figure, next, { replacing, pasted });
79
79
  if (replacing) say(t("已替换文件 · Ctrl+Z 撤销"));
80
80
  }
81
81
 
@@ -1,4 +1,5 @@
1
1
  import { headingIcon } from "../icons.mjs";
2
+ import { ALIGN_TOGGLES } from "../align.mjs";
2
3
  import { t } from "../../../shared/i18n/index.mjs";
3
4
 
4
5
  // 标题 1–4: headings h1–h4 (the fourth is body-sized, set apart by its weight; an author's h5 or h6 is
@@ -10,7 +11,7 @@ import { t } from "../../../shared/i18n/index.mjs";
10
11
  // heading into a paragraph), clipboard/own/ and src/transfer/markdown.mjs (written by its level: `clipboard`).
11
12
  // Folding its section: page/fold/ (the triangle right of its words, what is hidden, the section going with
12
13
  // it wherever blocks are taken away whole) and page.mjs (Ctrl/⌘+Enter in its words).
13
- // Its color (颜色 in the block menu): class switches, colors.mjs.
14
+ // Its color (颜色 in the block menu): class switches, colors.mjs; its alignment and indent (对齐与缩进), align.mjs.
14
15
  // Its look: the kit's styles/flow.css (src/render/kit/).
15
16
  // How a heading of a level is written out: # … #### by level.
16
17
  const writtenAs = (level) => ({ kind: "heading", level });
@@ -23,7 +24,7 @@ export default {
23
24
  heading: {
24
25
  name: t("标题"),
25
26
  match: [{ rank: 190, tag: ["h1", "h2", "h3", "h4", "h5", "h6"] }],
26
- colors: ["text", "background"],
27
+ colors: ["text", "background"], toggles: ALIGN_TOGGLES,
27
28
  emptyIsNothing: true, fromWords: true,
28
29
  clipboard: { of: (unit) => writtenAs(Math.min(4, Number(unit.localName.slice(1)) || 2)) }, // h5 and h6, which no type stands for, as heading 4
29
30
  },
@@ -31,22 +32,22 @@ export default {
31
32
  types: {
32
33
  heading1: {
33
34
  turn: 3, menu: ["basic", 3],
34
- tag: "h1", kind: "heading", leaf: "heading1", text: true, empty: true, placeholder: t("标题 1"), clipboard: writtenAs(1),
35
+ tag: "h1", kind: "heading", leaf: "heading1", text: true, empty: true, emptyToText: true, placeholder: t("标题 1"), clipboard: writtenAs(1),
35
36
  label: t("标题 1"), icon: headingIcon(1), hint: "#", markdown: [{ match: /^(#|#)[ \u00a0\u3000]$/ }], words: "标题1 一级标题 大标题 biaoti1 yiji bt1 bt heading1 h1 title",
36
37
  },
37
38
  heading2: {
38
39
  turn: 4, menu: ["basic", 4],
39
- tag: "h2", kind: "heading", leaf: "heading2", text: true, empty: true, placeholder: t("标题 2"), clipboard: writtenAs(2),
40
+ tag: "h2", kind: "heading", leaf: "heading2", text: true, empty: true, emptyToText: true, placeholder: t("标题 2"), clipboard: writtenAs(2),
40
41
  label: t("标题 2"), icon: headingIcon(2), hint: "##", markdown: [{ match: /^(##|##)[ \u00a0\u3000]$/ }], words: "标题2 二级标题 biaoti2 erji bt2 bt heading2 h2",
41
42
  },
42
43
  heading3: {
43
44
  turn: 5, menu: ["basic", 5],
44
- tag: "h3", kind: "heading", leaf: "heading3", text: true, empty: true, placeholder: t("标题 3"), clipboard: writtenAs(3),
45
+ tag: "h3", kind: "heading", leaf: "heading3", text: true, empty: true, emptyToText: true, placeholder: t("标题 3"), clipboard: writtenAs(3),
45
46
  label: t("标题 3"), icon: headingIcon(3), hint: "###", markdown: [{ match: /^(###|###)[ \u00a0\u3000]$/ }], words: "标题3 三级标题 小标题 biaoti3 sanji bt3 bt heading3 h3",
46
47
  },
47
48
  heading4: {
48
49
  turn: 6, menu: ["basic", 6],
49
- tag: "h4", kind: "heading", leaf: "heading4", text: true, empty: true, placeholder: t("标题 4"), clipboard: writtenAs(4),
50
+ tag: "h4", kind: "heading", leaf: "heading4", text: true, empty: true, emptyToText: true, placeholder: t("标题 4"), clipboard: writtenAs(4),
50
51
  label: t("标题 4"), icon: headingIcon(4), hint: "####", markdown: [{ match: /^(####|####)[ \u00a0\u3000]$/ }], words: "标题4 四级标题 biaoti4 siji bt4 bt heading4 h4",
51
52
  },
52
53
  },
@@ -32,6 +32,9 @@ export const SECTION_ICON = svg('<path d="M6 3.75h-.75a2 2 0 0 0-2 2v8.5a2 2 0 0
32
32
  */
33
33
  export const BLOCK_ICON = svg('<rect x="3.25" y="3.25" width="13.5" height="13.5" rx="2.5"/><path d="M6.75 7h3.5M6.75 10h6.5M6.75 13h4.5"/>');
34
34
 
35
+ /** 设置: a ring with eight short rays, the board's card menu and a custom block's handle menu. */
36
+ export const SETTINGS_ICON = svg('<circle cx="10" cy="10" r="2.25"/><path d="M10 3.25v1.5M10 15.25v1.5M3.25 10h1.5M15.25 10h1.5M5.2 5.2l1.06 1.06M13.74 13.74l1.06 1.06M5.2 14.8l1.06-1.06M13.74 6.26l1.06-1.06"/>');
37
+
35
38
  /** A detail card (详情卡片): a card with two lines of text. */
36
39
  export const DIALOG_ICON = svg('<rect x="3.25" y="3.75" width="13.5" height="12.5" rx="2.5"/><path d="M6.5 8h7M6.5 11.5h4.5"/>');
37
40
  export const LINK_ICON = svg('<g transform="rotate(45 10 10)"><path d="M7 8.5V6a3 3 0 0 1 6 0v2.5M7 11.5V14a3 3 0 0 0 6 0v-2.5M10 7.5v5"/></g>'); // two U's and the line between them, turned 45°
@@ -36,6 +36,8 @@ export default {
36
36
  name: t("图片"),
37
37
  match: [{ rank: 45, tag: "figure", class: "doc-image" }],
38
38
  boxed: true,
39
+ // Selected alone with its picture, its corner grips and toolbar show it: no selection fill (selection/index.mjs).
40
+ unfilled: ":scope > img",
39
41
  toggles: ALIGN,
40
42
  sizes: { [SPAN]: SPAN_SIZE },
41
43
  },
@@ -1,6 +1,7 @@
1
1
  import { MEDIA_CLASS } from "../../../shared/components/index.mjs";
2
+ import { IN_LAYER } from "../../../shared/layers.mjs";
2
3
  import { SPAN, SPAN_SIZE, UPLOAD_LIMITS, sizeLimit } from "../../../shared/media.mjs";
3
- import { gestureLock } from "../../page/gesture.mjs";
4
+ import { cancelOnEscape, endsGesture, gestureLock } from "../../page/gesture.mjs";
4
5
  import { openCrop } from "./crop.mjs";
5
6
  import { openViewer } from "./viewer.mjs";
6
7
  import { svg } from "../icons.mjs";
@@ -25,33 +26,68 @@ import { zoomable } from "../../../shared/zoom.mjs";
25
26
  // a click (../media/page.mjs): a file (an image, 10 MB at most) goes to the report's folder, a link's picture is
26
27
  // downloaded there by the server (an export never reaches the web); either is one undo step. The
27
28
  // picture shows where the server serves the folder; what the file is stays in data-ge-src.
28
- // With the pointer on a picture, Notion's toolbar stands at its top right (对齐, 标题, 展开, 更多:
29
- // the block menu) and a handle at each side; dragging one resizes the picture (a share of the reading
30
- // width, at most the width it stands in, both sides at once while centered; snapping to the whole of
31
- // that width, a share of 1: styles/sizes.css), one undo step on
32
- // letting go (Esc puts it back). 标题 shows its caption (empty ones show only then) with the caret
33
- // in it; Enter there makes a paragraph after the image. A double-click on the picture (or on any
34
- // other picture that opens large: a table's, a card's), or 展开, shows it alone over the page (a
35
- // click or Esc closes it). The block menu has 替换图片, 标题, 对齐 ›
36
- // and 恢复原始大小 (menuItems). A picture pasted (an image file on the clipboard) becomes an image
37
- // below the block the caret is in (`acceptsFile`, `fromFiles`).
29
+ // A click on a picture selects its image (`click`); while it is the one block selected (however it
30
+ // was: the click, its handle, the keys, a picture just put in) a short gray L stands at each of the
31
+ // picture's corners (the user's, 2026-11-09, feishu-lite's four corner handles drawn as Ls) and the
32
+ // toolbar at its top right, moved in clear of them (对齐, 标题, 展开, 更多: the block menu), both
33
+ // placed in the block tools' frames (`blockTools` follow, ../../page/blocks.mjs). Dragging a corner
34
+ // resizes the picture, its shape kept (feishu-lite's: by how far the pointer moves across, the left
35
+ // corners the other way, twice while centered), a share of the reading width, at most the width it
36
+ // stands in, snapping to the whole of that width (a share of 1: styles/sizes.css), one undo step on
37
+ // letting go (Esc puts it back). An L is a gray line with a thin edge of the page's color around it, so it
38
+ // reads on a black picture in light mode and a white one in dark mode (the old side bars, a gray of
39
+ // the page's ink, did not). 标题 shows its caption (empty ones show only then) with the caret in it;
40
+ // Enter there makes a paragraph after the image. A double-click on the picture (or on any other picture
41
+ // that opens large: a table's, a card's), or 展开, shows it alone over the page (a click or Esc closes
42
+ // it). The block menu has what the toolbar has not: 同步宽度到全局 first (every other image of the
43
+ // page as wide as this one, one undo step), 替换图片, 恢复原始大小, 裁剪图片 and 下载 (menuItems). A
44
+ // picture pasted (an image file on the clipboard) becomes an image below the block the caret is in
45
+ // (`acceptsFile`, `fromFiles`).
38
46
 
39
47
  const MAX_BYTES = UPLOAD_LIMITS.image;
40
48
  // 裁剪图片's row: a crop mark, drawn as the menus' other icons are.
41
49
  const CROP_ICON = svg('<path d="M6 2.75V13a1 1 0 0 0 1 1h10.25"/><path d="M2.75 6H13a1 1 0 0 1 1 1v10.25"/>');
50
+ // 同步宽度到全局's row: a width (an arrow both ways) over two pictures as wide as each other.
51
+ const SYNC_ICON = svg('<path d="M4 4.25h12M6 2.5 4 4.25 6 6M14 2.5l2 1.75L14 6"/><rect x="3.75" y="8.25" width="12.5" height="3.5" rx="1"/><rect x="3.75" y="14" width="12.5" height="3.5" rx="1"/>');
52
+
53
+ // A corner's grip: a box of GRIP px, the picture's corner AT px in from the box's outer corner, an L
54
+ // whose lines run ARM px along the picture's two edges from its corner, centered a little outside them.
55
+ const GRIP = 24;
56
+ const AT = 8;
57
+ const ARM = 12;
58
+ const L_PATH = `M${AT - 1} ${AT - 1 + ARM}V${AT - 1}h${ARM}`; // drawn for the top left; the others mirror it
59
+ // The corners: which way each mirrors the top left's L, and its cursor.
60
+ const CORNERS = {
61
+ nw: { x: "left", y: "top", cursor: "nwse" },
62
+ ne: { x: "right", y: "top", cursor: "nesw" },
63
+ sw: { x: "left", y: "bottom", cursor: "nesw" },
64
+ se: { x: "right", y: "bottom", cursor: "nwse" },
65
+ };
66
+ const INSET = 16; // how far the toolbar stands from the picture's top and from its right edge, the same each way (the user's, 2026-11-09: the top as far as the right): clear of the L's arms and of the top right grip's box, which reaches GRIP - AT = 16px in
67
+ const GAP = 6; // how far it stands above the picture when it does not fit in
42
68
 
43
69
  const STYLE = `
44
70
  /* The placeholder of an image with no picture yet (Notion's 605415): every empty block's, served with the page (../placeholder.mjs). */
45
71
  [data-ge-editing] .doc-image > img { cursor: default; }
46
- /* The handles (Notion's 681598); the toolbar is the media blocks' (../media/toolbar.mjs). */
47
- .ge-layer > .ge-image-handle { pointer-events: auto; }
72
+ /* The toolbar is the media blocks' (../media/toolbar.mjs). */
48
73
  .ge-image-align { display: flex; align-items: center; padding: 2px; border-radius: 6px; background: var(--c-popBac); box-shadow: var(--c-shaOutLg); }
49
74
  .ge-image-align-sep { width: 1px; height: 16px; margin: 0 2px; background: var(--ca-borSecTra); }
50
- .ge-image-handle { display: flex; align-items: center; justify-content: center; width: 15px; cursor: col-resize; }
51
- /* A plain gray bar, no border (the embed's corners the same). */
52
- .ge-image-handle::after { content: ""; width: 4px; height: 40px; max-height: 50%; border-radius: 4px; background: light-dark(rgb(55 53 47 / 35%), rgb(255 255 255 / 38%)); transition: background 120ms ease; }
53
- .ge-image-handle:hover::after, html[data-ge-image-resize] .ge-image-handle::after { background: light-dark(rgb(55 53 47 / 60%), rgb(255 255 255 / 62%)); }
54
- ${gestureLock("data-ge-image-resize", { cursor: "col-resize" })}
75
+ /* The corners' grips: a short gray L, round at its ends, no edge round it (the user, 2026-11-09): a
76
+ neutral gray that reads on a black picture and a white one alike (4.5 to 1 on each), with a soft
77
+ shadow for a picture of its own gray; darker (lighter) under the pointer and while it is dragged.
78
+ No border, no fill. */
79
+ .ge-layer > .ge-image-grip { pointer-events: auto; z-index: ${IN_LAYER.button}; } /* over the image's fill: it is selected */
80
+ .ge-image-grip { width: ${GRIP}px; height: ${GRIP}px; --ge-grip-line: #767676; filter: drop-shadow(0 0 2px rgb(0 0 0 / 42%)); }
81
+ .ge-image-grip[data-cursor="nwse"] { cursor: nwse-resize; }
82
+ .ge-image-grip[data-cursor="nesw"] { cursor: nesw-resize; }
83
+ .ge-image-grip svg { display: block; width: ${GRIP}px; height: ${GRIP}px; fill: none; stroke-linecap: round; stroke-linejoin: round; pointer-events: none; }
84
+ .ge-image-grip[data-corner="ne"] svg { transform: scaleX(-1); }
85
+ .ge-image-grip[data-corner="sw"] svg { transform: scaleY(-1); }
86
+ .ge-image-grip[data-corner="se"] svg { transform: scale(-1); }
87
+ .ge-image-grip-line { stroke: var(--ge-grip-line); stroke-width: 3; transition: stroke 120ms ease; }
88
+ .ge-image-grip:hover, .ge-image-grip[data-on] { --ge-grip-line: light-dark(#4f4d49, #d3d1cb); }
89
+ ${gestureLock('data-ge-image-resize="nwse"', { cursor: "nwse-resize", select: true })}
90
+ ${gestureLock('data-ge-image-resize="nesw"', { cursor: "nesw-resize", select: true })}
55
91
  `;
56
92
 
57
93
  const ALIGNS = [["left", t("左对齐"), "alignLeft"], ["center", t("居中对齐"), "alignCenter"], ["right", t("右对齐"), "alignRight"]];
@@ -156,103 +192,110 @@ export function create({ pages, assetUrl }) {
156
192
  if (replacing && !quiet) say(t("已替换图片 · Ctrl+Z 撤销"));
157
193
  }
158
194
 
159
- // ---------- the toolbar and the handles ----------
195
+ // ---------- the toolbar and the corners' grips ----------
160
196
 
161
- let hovered = null; // the image the pointer is on
162
- let resizing = null; // { figure, side, x, start, room, before, centered }
197
+ let shown = null; // the image whose tools show: the one block selected
198
+ let resizing = null; // { figure, corner, x, start, room, page, before, centered }
199
+ let aligning = null; // the 对齐 popover open, a press in which keeps the image selected
163
200
  const bar = mediaBar(t("图片操作"));
164
- const tool = (label, tip, icon, run) => toolButton({ label, tip, icon, run, alive: () => hovered?.isConnected });
201
+ const tool = (label, tip, icon, run) => toolButton({ label, tip, icon, run, alive: () => shown?.isConnected });
165
202
  const tools = {
166
- align: tool(t("对齐方式"), t("设置区块对齐"), () => TOOL_ICONS[{ left: "alignLeft", center: "alignCenter", right: "alignRight" }[sideOf(hovered)]], (button) => alignPopup(hovered, button)),
167
- caption: tool(t("标题@caption"), t("添加标题"), TOOL_ICONS.caption, () => media.caption(hovered)),
168
- expand: tool(t("展开"), t("全屏查看"), TOOL_ICONS.expand, () => view(hovered)),
203
+ align: tool(t("对齐方式"), t("设置区块对齐"), () => TOOL_ICONS[{ left: "alignLeft", center: "alignCenter", right: "alignRight" }[sideOf(shown)]], (button) => alignPopup(shown, button)),
204
+ caption: tool(t("标题@caption"), t("添加标题"), TOOL_ICONS.caption, () => media.caption(shown)),
205
+ expand: tool(t("展开"), t("全屏查看"), TOOL_ICONS.expand, () => view(shown)),
169
206
  more: tool(t("更多"), t("打开区块操作菜单"), TOOL_ICONS.more, (button) => {
170
207
  const rect = button.getBoundingClientRect();
171
- openMenu(hovered, { left: rect.left, right: rect.right, top: rect.top, bottom: rect.bottom });
208
+ openMenu(shown, { left: rect.left, right: rect.right, top: rect.top, bottom: rect.bottom });
172
209
  }),
173
210
  };
174
211
  bar.append(...Object.values(tools));
175
- const handles = { left: element("div", { class: "ge-image-handle", "data-side": "left" }), right: element("div", { class: "ge-image-handle", "data-side": "right" }) };
212
+ const lines = `<svg viewBox="0 0 ${GRIP} ${GRIP}" aria-hidden="true"><path class="ge-image-grip-line" d="${L_PATH}"/></svg>`;
213
+ const grips = Object.fromEntries(Object.entries(CORNERS).map(([corner, { cursor }]) => {
214
+ const grip = element("div", { class: "ge-image-grip", "data-corner": corner, "data-cursor": cursor, "aria-hidden": "true" });
215
+ grip.innerHTML = lines;
216
+ return [corner, grip];
217
+ }));
218
+ const nodes = () => [bar, ...Object.values(grips)];
176
219
 
177
- // The toolbar and the handles over the picture of `figure`, or away.
220
+ // The toolbar and the grips over the picture of `figure`, or away. The toolbar stands at the
221
+ // picture's top right, INSET from its top and from its right edge; on a picture too narrow to hold
222
+ // it with that room each side, above the picture, its right edge on the picture's.
178
223
  function show(figure) {
179
- hovered = figure;
224
+ shown = figure;
180
225
  const img = figure && imageOf(figure);
181
226
  const shape = img && shapeOf(img);
182
227
  if (!shape) {
183
- hovered = null;
184
- for (const node of [bar, handles.left, handles.right]) node.remove();
228
+ shown = null;
229
+ for (const node of nodes()) node.remove();
185
230
  return;
186
231
  }
187
- const alive = () => hovered === figure && figure.isConnected;
188
- for (const node of [bar, handles.left, handles.right]) own(node, "image tools", { owner: figure, alive });
232
+ const alive = () => shown === figure && figure.isConnected;
233
+ for (const node of nodes()) own(node, "image tools", { owner: figure, alive });
189
234
  for (const button of Object.values(tools)) button.draw();
190
- const { rect } = shape;
235
+ const { rect, layer } = shape;
191
236
  tools.align.style.display = figure.style.getPropertyValue(SPAN).trim() === "1" ? "none" : ""; // a picture as wide as it can be has no side
192
- if (bar.parentNode !== shape.layer) shape.layer.append(bar); // in the page before its width is measured
193
- drawAt(bar, shape.layer, rect.right - 4 - bar.offsetWidth, rect.top + 4);
194
- for (const [side, handle] of Object.entries(handles)) {
195
- handle.style.height = `${Math.round(rect.bottom - rect.top)}px`;
196
- drawAt(handle, shape.layer, side === "left" ? rect.left : rect.right - 15, rect.top);
237
+ if (bar.parentNode !== layer) layer.append(bar); // in the page before its width is measured
238
+ const fits = rect.right - rect.left >= bar.offsetWidth + 2 * INSET;
239
+ drawAt(bar, layer, fits ? rect.right - INSET - bar.offsetWidth : rect.right - bar.offsetWidth, fits ? rect.top + INSET : rect.top - AT - GAP - bar.offsetHeight);
240
+ for (const [corner, grip] of Object.entries(grips)) {
241
+ const { x, y } = CORNERS[corner];
242
+ drawAt(grip, layer, x === "left" ? rect.left - AT : rect.right - GRIP + AT, y === "top" ? rect.top - AT : rect.bottom - GRIP + AT);
197
243
  }
198
244
  }
199
245
 
200
- document.addEventListener("mousemove", (event) => {
201
- if (resizing || [bar, handles.left, handles.right].some((node) => node.contains(event.target))) return;
202
- const figure = active() && !keptByComponent(event.target) ? event.target.closest?.("main figure.doc-image") ?? null : null;
203
- if (figure !== hovered) show(figure && imageOf(figure) ? figure : null);
204
- }, { passive: true, capture: true });
205
- addEventListener("scroll", () => { if (hovered && !resizing) show(hovered); }, { passive: true, capture: true });
206
- document.documentElement.addEventListener("mouseleave", () => { if (!resizing) show(null); });
207
- // The hovered picture taken off the page by an undo, a redo or a commit: the tools go at once, not at the next pointer move.
208
- onSettle(() => { if (hovered && !hovered.isConnected && !resizing) show(null); });
246
+ // The image the tools stand for, from the blocks selected (the block tools' frame): one image with
247
+ // its picture, not one a component keeps; while a corner is dragged, the one dragged.
248
+ function follow(picked) {
249
+ if (resizing) return show(resizing.figure);
250
+ const [one] = picked.length === 1 ? picked : [];
251
+ const figure = one?.matches?.("figure.doc-image") && imageOf(one) && active() && !keptByComponent(one) ? one : null;
252
+ if (figure || shown) show(figure);
253
+ }
254
+ // The picture taken off the page by an undo, a redo or a commit: the tools go at once, not at the next frame.
255
+ onSettle(() => { if (shown && !shown.isConnected && !resizing) show(null); });
209
256
 
210
- // Dragging a handle: the picture's width as a share of the reading width, at most the width it
211
- // stands in (1 when it reaches that), live; one undo step on letting go.
212
- for (const [side, handle] of Object.entries(handles)) {
213
- handle.addEventListener("pointerdown", (event) => {
214
- if (event.button !== 0 || !hovered) return;
257
+ // Dragging a corner: the picture's width as a share of the reading width, at most the width it
258
+ // stands in (1 when it reaches that), live, its height following (its shape kept); feishu-lite's
259
+ // rule (resize.ts): by how far the pointer moves across, the left corners the other way, twice while
260
+ // centered (it grows on both sides). One undo step on letting go; Esc, or the press cancelled, puts it back.
261
+ for (const [corner, grip] of Object.entries(grips)) {
262
+ grip.addEventListener("pointerdown", (event) => {
263
+ if (event.button !== 0 || !shown || resizing) return;
215
264
  event.preventDefault();
216
- const figure = hovered;
217
- const room = columnRoom(figure);
218
- resizing = { figure, side, x: event.clientX, start: imageOf(figure).getBoundingClientRect().width, room, page: readingRoom(figure), before: figure.style.getPropertyValue(SPAN), centered: sideOf(figure) === "center" };
219
- handle.setPointerCapture?.(event.pointerId);
220
- document.documentElement.setAttribute("data-ge-image-resize", "");
265
+ const figure = shown;
266
+ resizing = { figure, corner, x: event.clientX, start: imageOf(figure).getBoundingClientRect().width, room: columnRoom(figure), page: readingRoom(figure), before: figure.style.getPropertyValue(SPAN), centered: sideOf(figure) === "center" };
267
+ grip.setPointerCapture?.(event.pointerId);
268
+ grip.setAttribute("data-on", "");
269
+ document.documentElement.setAttribute("data-ge-image-resize", CORNERS[corner].cursor);
221
270
  });
222
- handle.addEventListener("pointermove", (event) => {
223
- if (!resizing) return;
271
+ grip.addEventListener("pointermove", (event) => {
272
+ if (resizing?.corner !== corner) return;
224
273
  const { figure, x, start, room, page, centered } = resizing;
225
- const delta = (event.clientX - x) * (side === "right" ? 1 : -1) * (centered ? 2 : 1);
274
+ const delta = (event.clientX - x) * (CORNERS[corner].x === "right" ? 1 : -1) * (centered ? 2 : 1);
226
275
  const width = Math.max(Math.min(room, page * SPAN_SIZE.min), Math.min(room, start + delta));
227
276
  figure.style.setProperty(SPAN, room - width <= SNAP ? "1" : String(Math.round((width / page) * 1000) / 1000));
228
277
  show(figure);
229
278
  });
230
- const end = (keep) => {
231
- if (!resizing) return;
232
- const { figure, before } = resizing;
233
- resizing = null;
234
- document.documentElement.removeAttribute("data-ge-image-resize");
235
- const after = figure.style.getPropertyValue(SPAN);
236
- if (before) figure.style.setProperty(SPAN, before);
237
- else figure.style.removeProperty(SPAN);
238
- if (keep && after !== before && look(figure, { sizes: { [SPAN]: after || null } })) say(t("图片宽度 {0}% · Ctrl+Z 撤销", Math.round(Number(after) * 100)));
239
- show(figure);
240
- };
241
- handle.addEventListener("pointerup", () => end(true));
242
- handle.addEventListener("pointercancel", () => end(false));
243
- handle.addEventListener("lostpointercapture", () => end(true));
279
+ grip.addEventListener("pointerup", () => endResize(true));
280
+ grip.addEventListener("pointercancel", () => endResize(false));
281
+ grip.addEventListener("lostpointercapture", () => endResize(true));
244
282
  }
245
- document.addEventListener("keydown", (event) => {
246
- if (event.key !== "Escape" || !resizing) return;
247
- event.preventDefault();
248
- event.stopPropagation();
283
+ // The drag over: the width it had back on the page, then (`keep`) the one dragged to as one undo step.
284
+ function endResize(keep) {
285
+ if (!resizing) return;
249
286
  const { figure, before } = resizing;
250
287
  resizing = null;
288
+ for (const grip of Object.values(grips)) grip.removeAttribute("data-on");
251
289
  document.documentElement.removeAttribute("data-ge-image-resize");
290
+ const after = figure.style.getPropertyValue(SPAN);
252
291
  if (before) figure.style.setProperty(SPAN, before);
253
292
  else figure.style.removeProperty(SPAN);
293
+ if (keep && after !== before && look(figure, { sizes: { [SPAN]: after || null } })) say(t("图片宽度 {0}% · Ctrl+Z 撤销", Math.round(Number(after) * 100)));
254
294
  show(figure);
255
- }, true);
295
+ }
296
+ // Esc gives the drag up and goes no further (the image stays selected); so do the window losing the focus and the page hidden.
297
+ cancelOnEscape({ active: () => Boolean(resizing), cancel: () => endResize(false) });
298
+ endsGesture({ active: () => Boolean(resizing), cancel: () => endResize(false), owns: () => false });
256
299
 
257
300
  // 对齐: Notion's small popover of the three sides, under its button.
258
301
  function alignPopup(figure, button) {
@@ -270,11 +313,29 @@ export function create({ pages, assetUrl }) {
270
313
  });
271
314
  const rect = button.getBoundingClientRect();
272
315
  const popup = openPopup({ origin: { left: rect.left, right: rect.right, top: rect.top, bottom: rect.bottom, width: rect.width, height: rect.height }, placement: "bottom", align: "center", gap: 8, content: el });
316
+ aligning = el;
273
317
  }
274
318
 
275
319
  function align(figure, side) {
276
320
  if (look(figure, sideLook(side))) say(t("已{0} · Ctrl+Z 撤销", ALIGNS.find(([each]) => each === side)[1]));
277
- if (hovered === figure) show(figure);
321
+ if (shown === figure) show(figure);
322
+ }
323
+
324
+ // 同步宽度到全局: every other image of the page (with its picture, not one a component keeps) as wide
325
+ // as this one shows, one undo step (undo selects this one again), written as one change. The width is
326
+ // what the picture shows at as a share of the reading width (styles/sizes.css), so a picture with no
327
+ // width of its own, or held narrower by its column, passes on the width it is seen at; the whole
328
+ // reading width is 1. This one is left as it is.
329
+ const others = (figure) => [...document.querySelectorAll("main figure.doc-image")].filter((each) => each !== figure && imageOf(each) && each.matches("[data-ge-unit], [data-ge-new]") && !keptByComponent(each));
330
+ function syncWidths(figure) {
331
+ const img = imageOf(figure);
332
+ if (!img?.isConnected) return;
333
+ const page = readingRoom(figure);
334
+ const width = img.getBoundingClientRect().width;
335
+ const share = page - width <= SNAP ? "1" : String(Math.max(SPAN_SIZE.min, Math.round((width / page) * 1000) / 1000));
336
+ const targets = others(figure).filter((each) => each.style.getPropertyValue(SPAN).trim() !== share);
337
+ if (targets.length === 0 || !look(targets, { sizes: { [SPAN]: share } }, figure)) return say(t("页面上的图片已经都是这个宽度。"));
338
+ say(t("已将 {0} 张图片设为同一宽度 · Ctrl+Z 撤销", targets.length));
278
339
  }
279
340
 
280
341
  // ---------- the viewer ----------
@@ -339,18 +400,18 @@ export function create({ pages, assetUrl }) {
339
400
  }, true);
340
401
  // ---------- the block menu, pasting ----------
341
402
 
342
- /** An image's rows in its block menu. */
403
+ /**
404
+ * An image's rows in its block menu: what its toolbar has not (标题, 对齐 and 展开 are there, and the
405
+ * toolbar shows whenever the image is the one block selected, as it is while its menu is open):
406
+ * 同步宽度到全局 first (where the page has another picture), 替换图片, 恢复原始大小, 裁剪图片, 下载.
407
+ */
343
408
  function menuItems(target) {
344
409
  if (!target?.matches?.("figure.doc-image") || !imageOf(target)) return [];
345
- const rows = [
346
- { label: t("替换图片"), icon: ICONS.reset, run: () => setTimeout(() => pick(target)) },
347
- { label: t("标题@caption"), icon: TOOL_ICONS.caption, run: () => setTimeout(() => media.caption(target)) },
348
- { label: t("对齐"), icon: TOOL_ICONS[{ left: "alignLeft", center: "alignCenter", right: "alignRight" }[sideOf(target)]], width: 180,
349
- submenu: () => ALIGNS.map(([side, label, name]) => ({ label, icon: TOOL_ICONS[name], current: side === sideOf(target), run: () => align(target, side) })) },
350
- ];
410
+ const rows = [];
411
+ if (others(target).length > 0) rows.push({ label: t("同步宽度到全局"), icon: SYNC_ICON, run: () => syncWidths(target) });
412
+ rows.push({ label: t("替换图片"), icon: ICONS.reset, run: () => setTimeout(() => pick(target)) });
351
413
  if (target.style.getPropertyValue(SPAN)) rows.push({ label: t("恢复原始大小"), icon: ICONS.reset, run: () => { if (look(target, { sizes: { [SPAN]: null } })) say(t("已恢复原始大小 · Ctrl+Z 撤销")); } });
352
414
  rows.push({ label: t("裁剪图片"), icon: CROP_ICON, run: () => setTimeout(() => crop(target)) });
353
- rows.push({ label: t("全屏查看"), icon: TOOL_ICONS.expand, run: () => view(target) });
354
415
  rows.push({ label: t("下载"), icon: TOOL_ICONS.download, run: () => download(imageOf(target)) });
355
416
  return rows;
356
417
  }
@@ -380,6 +441,27 @@ export function create({ pages, assetUrl }) {
380
441
  pick(figure);
381
442
  return true;
382
443
  },
383
- get nodes() { return [bar, handles.left, handles.right].filter((node) => node.isConnected); },
444
+ /**
445
+ * A click on an image's picture selects the image (its toolbar and grips then show: `blockTools`),
446
+ * the second of a double-click too (its press let go of it; the double-click itself is the
447
+ * viewer's). Returns whether it took the click.
448
+ */
449
+ click(event) {
450
+ const img = event.target;
451
+ const figure = img?.localName === "img" ? img.parentElement : null;
452
+ if (event.button !== 0 || !figure?.matches("main figure.doc-image") || imageOf(figure) !== img || !active() || keptByComponent(figure)) return false;
453
+ media.select(figure);
454
+ return true;
455
+ },
456
+ /** The toolbar and the grips as a part of the block tools: placed in their frames by the blocks selected (`follow`), a press on them keeping the selection (`holds`). */
457
+ blockTools: () => [{
458
+ follow,
459
+ holds: (node) => nodes().some((each) => each.contains(node)) || Boolean(aligning?.isConnected && aligning.contains(node)),
460
+ hide() {
461
+ endResize(false);
462
+ show(null);
463
+ },
464
+ }],
465
+ get nodes() { return nodes().filter((node) => node.isConnected); },
384
466
  };
385
467
  }
@@ -2,7 +2,7 @@ import { escapeText } from "../../../shared/escape.mjs";
2
2
  import { ATTRIBUTE, FALLBACK, INSTANCE, PACKAGE_ID, SLOT, SLOT_PART, componentAt, heldBy, instanceMatch, refusedInCopy, slotsIn } from "../../../shared/components/installed.mjs";
3
3
  import { wordsIn } from "../../page/clipboard/own/writers.mjs";
4
4
  import { BLOCK_ICON } from "../icons.mjs";
5
- import { VIEW_FROM } from "../../../shared/table-chart.mjs";
5
+ import { CHART_KEY, VIEW_FROM } from "../../../shared/table-chart.mjs";
6
6
  import { t } from "../../../shared/i18n/index.mjs";
7
7
 
8
8
  // 组件: the one kind of block every component package is to the engines, the host of a package's
@@ -109,6 +109,10 @@ export default {
109
109
  boxed: true,
110
110
  chapter: true,
111
111
  copy: "itself",
112
+ // The key of the table a view reads (VIEW_FROM), which a view's 设置 changes on an instance the file
113
+ // has (the commit writes it in its start tag, as a table's CHART_SOURCE); one made on the page says it
114
+ // by `extras` below.
115
+ attributes: { [VIEW_FROM]: (value) => CHART_KEY.test(value) },
112
116
  },
113
117
  },
114
118
  boxes: {
@@ -131,7 +135,7 @@ export default {
131
135
  // Its markup is the file's, from its template or what was carried (`data-ge-copy`, its slots
132
136
  // empty); what its slots hold goes apart, blocks made on the page as any (`children`, each in the
133
137
  // slot `boxKeys` names), so that the commit's answer names each, as for a block made in a column.
134
- // A view of a table (a table's 视图 › made it of its template, or a paste carried one) says the key
138
+ // A view of a table (a menu made it of its template, bound to the table above it, or a paste carried one) says the key
135
139
  // of the table it reads as the page has it (`from`, its VIEW_FROM, which a paste may have given it
136
140
  // anew: clipboard/own/chart-keys.mjs), which the file writes on it (file.mjs source); one carried
137
141
  // with none on it says null (a view pasted into another document, its key taken off there), so